package cz.cuni.amis.pogamut.episodic.episodes;

import cz.cuni.amis.pogamut.episodic.memory.Affordance;
import cz.cuni.amis.pogamut.episodic.memory.AgentMemory;
import cz.cuni.amis.pogamut.episodic.memory.IdGenerator;
import cz.cuni.amis.pogamut.episodic.memory.Parameters;
import java.io.Serializable;
import java.util.*;

/**
 * Chronology is a Collection of ChronoBags. It forms a rough time
 * index of all events stored in it.
 *
 * @author  Zdenek Behan
 */
public class ChronoLogy implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * A reference to ID generator structure, that will provide
     * the unique IDs to all inside node. This unique IDs are mostly
     * needed when identifying the visualized vertices (because nodes
     * can have identical names).
     */
    public IdGenerator idGenerator;

    /**
     * Parent memory structure to which an instance belongs.
     */
    public AgentMemory mem;

    /**
     * Current chronobag.
     */
    public Chronobag present;

    /**
     * List of all the past chronobags.
     */
    public HashSet<Chronobag> past = new HashSet<Chronobag>();

    // Indexed by chronobag level
    Map<Integer ,Chronobag> pastSequenceEnds = new HashMap<Integer, Chronobag>();

    public ChronoLogy(AgentMemory _mem, IdGenerator _idGenerator) {
        mem = _mem;
        idGenerator = _idGenerator;

        present = new Chronobag(idGenerator, this);
        present.objectNodes = new HashMap<String, ObjectNode>();
    }

    public boolean finishNode(String node, ArrayList<String> trace, boolean success) throws Exception {
        return present.finishNode(node, trace, success);
    }

    public boolean addNewNode(String nodeName, ArrayList<String> trace, ArrayList<Affordance> affordances, long time) throws Exception {
        return present.addNewNode(nodeName, trace, affordances, time);
    }

    private void ensureFullHierarchy() {
        // Pick the most recent past bag and create a full hierarchy going
        // up, as needed.
        Chronobag last = present.older;
        // Assuming intervals are congruential, we just need to find the first
        // existing parent, that parent will most certainly have its own
        // parents in order.
        if(last != null) {
            if (last.parent != null) {
                // Sanity check, do nothing here.
                return;
            }
            // Last does not have a parent, so it was probably just created.
            // Look to the older brother, and see if their parent is also our parent,
            // otherwise create one and iterate.
            while ((last.parent == null) && (last.level < (mem.parameters.MAX_CHRONOBAG_LEVELS-1))) {
                if ((last.older != null) && (last.older.parent != null) &&
                   ((last.older.parent.begin >= last.begin) && (last.older.parent.end <= last.end))) {
                    // If brother's parent covers our timespan, there is nothing else to do.
                    last.parent = last.older.parent;
                    // We have a parent that exists, that means we're a new child and should let them know.
                    int i = 0;
                    for(Chronobag ch: last.parent.children) {
                        if(ch == null) {
                            assert(i>0); // We can't be the first child here.
                            // Settle down in the first empty spot.
                            last.parent.children[i] = last;
                            return;
                        }
                        i++;
                    }
                    // TODO: If we get here, it's really bad. Throw an exception maybe?
                    return;
                }
                last.parent = new Chronobag(last.idGenerator, last.chrono, last.level+1);
                past.add(last.parent);
                pastSequenceEnds.put(last.level+1, last.parent);
                // Set time span.
                last.parent.begin = last.begin;
                last.parent.end = last.begin - mem.parameters.CHRONOBAG_INTERVAL_LENGHTS[last.level+1] + 1;
                last.parent.children[0] = last;
                // Hook up parent to his neighbours
                if (last.older != null) {
                    // On the borderline, neighbours are pretty easy.
                    last.parent.older = last.older.parent;
                }
                last = last.parent;
            }
        }
    }

    public void performSleep() {
        HashMap<String, ObjectNode> objNodes = present.objectNodes;
        Chronobag last = present;
        Chronobag previousLast = present;
        present.finish();
        past.add(present);
        // Create new present, link them together, move on.
        present.younger = new Chronobag(idGenerator, this);
        present.younger.isPresent = true;
        present.younger.older = present;
        present = present.younger;
        // Fix past sequence.
        pastSequenceEnds.put(0, present);
        present.objectNodes = objNodes;

        for (Chronobag c : past) {
            c.increaseDay();
        }
        // Create empty chronobags where they might be missing.
        ensureFullHierarchy();

        if (!mem.parameters.NO_ABSTRACT_CHRONOBAGS) {
            copyEpisodesToAbstractChronobags();
        }

        // Episode forgetting.
        if (!mem.parameters.NO_FORGETTING) {
            Chronobag c;
            for (Iterator<Chronobag> it = past.iterator(); it.hasNext();) {
                c = it.next();
                if (c.isLandmark()) continue;
                c.decideToForgetNodes(c.isPresent);
                if (c.getEpisodes().isEmpty()) {
                    boolean rv = true;
                    if (c.children != null)
                        for (Chronobag ch : c.children)
                            rv &= (ch == null);
                    if (rv)
                        c.deleteChronobag();
                    if (c == pastSequenceEnds.get(c.level)) {
                        pastSequenceEnds.put(c.level, c.older);
                    }
                    it.remove();
                }
            }
        }

        // De-indicate presentness.
        previousLast.isPresent = false;

        // Episode merging, called on all past chronobags
        if (!mem.parameters.NO_EPISODE_MERGING) {
            for (Chronobag c : past) {
               c.mergeEpisodes();
            }
        }
    }

    /**
     * Copies all episodes from daily chronobag to more abstract chronobags
     * and updates the maxNumberOfNodes variable for chronobags.
     */
    private void copyEpisodesToAbstractChronobags() {
     // this was original version that went over all the chronobags and copied
     // the episodes passing specified threshold.
     /*   Chronobag c;
        for (int i = 0; i < Parameters.MAX_CHRONOBAG_LEVELS - 1; i++) {
            c = pastSequenceEnds.get(i);
            while (c != null) {
                if (c.isLandmark()) break;

                for (Episode e : c.getEpisodes()) {
                    if (e.calculateCopyScore() > Parameters.EPISODE_COPY_THRESHOLD_SCORE) {
                        c.copyEpisodeToAbstractChronobag(e);
                    }
                }

                c = c.getOlderChronobag();
            }
        }*/

        Chronobag c = present;
        assert (c.getEpisodes().isEmpty());
        c = c.older;
        for (int i = 0; i < mem.parameters.MAX_CHRONOBAG_LEVELS - 1; i++) {
            for (Episode e : c.getEpisodes()) {
                c.copyEpisodeToAbstractChronobag(e);
            }
            c = c.parent;
        }

        c = present.older;
        Chronobag ch = c;
        while (c != null) {
            c.increaseMaxNumberOfNodes(ch);
            c = c.parent;
        }
    }

    public void reviewActiveIntentions(ArrayList<String> list) {
        present.reviewActiveEpisodes(list);
    }

    /********************** Episode search tools ********************/

    /**
     * Collects a collection of episodes from the given top-level bag. The
     * collection is weakly ordered.
     * @param bag
     * @return
     */
    public Collection<Episode> getEpisodeCollection(Chronobag bag) {
        Collection<Episode> col = new ArrayList<Episode>();
        col.addAll(bag.getEpisodes());
        if (bag.children == null)
            return col;
        for (Chronobag ch : bag.children) {
            if (ch != null) {
                col.addAll(getEpisodeCollection(ch));
            }
        }
        return col;
    }
    // Same but global
    public Collection<Episode> getEpisodeCollection() {
        ArrayList<Episode> col = new ArrayList<Episode>();
        Chronobag ch = this.pastSequenceEnds.get(Parameters.MAX_CHRONOBAG_LEVELS-1);
        while (ch != null) {
            col.addAll(0, getEpisodeCollection(ch));
            ch = ch.older;
        }
        return col;
    }

    // Selectors for various conditions
    public Collection<Episode> episodeLevelSelector(Collection<Episode> c, int level) {
        Iterator<Episode> it = c.iterator();
        Episode e;
        while(it.hasNext()) {
            e = it.next();
            if(e.getParentChronobag().level != level) {
                it.remove();
            }
        }

        return c;
    }

    public Collection<Episode> episodeRootnameSelector(Collection<Episode> c, String rootname) {
        Iterator<Episode> it = c.iterator();
        Episode e;
        while(it.hasNext()) {
            e = it.next();
            if(!e.root.getName().equalsIgnoreCase(rootname)) {
                it.remove();
            }
        }

        return c;
    }

    /**
     * Get chronobags collection.
     * @return
     */
    // Get all bags.
    public Collection<Chronobag> getChronobags() {
        Collection<Chronobag> col = new HashSet<Chronobag>();
        col.add(present);
        col.addAll(past);
        return col;
    }
    // Get bags of particular level.
    public Collection<Chronobag> getChronobagsChronology(int level) {
        ArrayList<Chronobag> col = new ArrayList<Chronobag>();

        Chronobag ch = pastSequenceEnds.get(Parameters.MAX_CHRONOBAG_LEVELS-1);
        while (ch != null) {
            // This means we're picking the highest level
            col.addAll(0, getChronobagsChronology(level, ch));
            ch = ch.older;
        }
        return col;
    }
    public Collection<Chronobag> getChronobagsChronology(int level, Chronobag ch) {
        ArrayList<Chronobag> col = new ArrayList<Chronobag>();

        if (ch == null) {
            return col;
        }
        if (ch.level == level ) {
            col.add(ch);
            return col;
        }
        if (ch.level == level + 1) {
            // We're the parent of the ones we want. Return all the children.
            for (Chronobag c : ch.children) {
                if (c != null)
                    col.add(c);
            }
        } else {
            for (Chronobag c : ch.children) {
                if (c != null)
                    col.addAll(getChronobagsChronology(level, c));
            }
        }
        return col;
    }

    public Map<Integer ,Chronobag> getChronobagSequenceEnds() {
        return pastSequenceEnds;
    }

    public Chronobag getPresentChronobag() {
        return present;
    }

    public HashSet<Chronobag> getPastChrononags() {
        return past;
    }


    public Integer getLastEpisodeId() {
        return present.getLastEpisodeId();
    }
}
